package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const (
	chromiumBuildbot = "http://build.chromium.org/p/chromium/json"
	// Buildbot/builders/<builder>/builds/<buildnum> => BuildData
)

var (
	bugRegexp         = regexp.MustCompile("\nBUG=([0-9, ]+)")
	reviewUrlRegexp   = regexp.MustCompile("\nReview URL: ([^ ]+)")
	reviewIssueRegexp = regexp.MustCompile("http://.*/([0-9]+)$")
	flagBuilder       string
	flagBuildnum      int
)

func init() {
	flag.StringVar(&flagBuilder, "builder", "", "builder name")
	flag.IntVar(&flagBuildnum, "buildnum", -1, "build number")
}

func parseChangeDescription(s string) (summary, bug, review string) {
	summary = s
	if n := strings.Index(s, "\n"); n > 0 {
		summary = s[:n]
	}
	if m := bugRegexp.FindStringSubmatch(s); m != nil {
		bug = m[1]
	}
	if m := reviewUrlRegexp.FindStringSubmatch(s); m != nil {
		review = m[1]
	}
	return
}

func getReviewIssue(review string) (issue int64) {
	if m := reviewIssueRegexp.FindStringSubmatch(review); m != nil {
		issue, _ = strconv.ParseInt(m[1], 10, 0)
	}
	return
}

// activity.go
const (
	ActivityBuildStart   = "BuildStart"
	ActivityBuildFailed  = "BuildFailed"
	ActivityBuildSuccess = "BuildSuccess"
)

type Activity struct {
	User    string
	Time    time.Time
	URL     string
	Snippet string
	Type    string
	Issues  []int64

	// ActivityRecipients
	Recipients []string

	// ActivityFiles
	Files []string
}

// Buildbot data

type BuilderData struct {
	Basedir       string    `json: "basedir"`
	CachedBuilds  []float64 `json: "cachedBuilds"`
	Category      string    `json: "category"`
	CurrentBuilds []float64 `json: "currentBuilds"`
	PendingBuilds float64   `json: "pendingBuilds"`
	Slaves        []string  `json: "slaves"`
	State         string    `json: "state"`
}

func (builder BuilderData) GetLatestBuild() (buildnum int64) {
	for _, build := range builder.CurrentBuilds {
		if int64(build) > buildnum {
			buildnum = int64(build)
		}
	}
	for _, build := range builder.CachedBuilds {
		if int64(build) > buildnum {
			buildnum = int64(build)
		}
	}
	return buildnum
}

func (builder BuilderData) GetLastBuild() (buildnum int64) {
	current := map[int64]bool{}
	for _, build := range builder.CurrentBuilds {
		current[int64(build)] = true
	}
	for _, build := range builder.CachedBuilds {
		if !current[int64(build)] && int64(build) > buildnum {
			buildnum = int64(build)
		}
	}
	return buildnum
}

type ProjectData struct {
	BuildbotURL string `json: "buildbotURL"`
	ProjectName string `json: "projectName"`
	ProjectURL  string `json: "projectURL"`
}

type StepData struct {
	Eta float64 `json: "eta"`
	// Expectations
	IsFinished bool          `json: "isFinished"`
	IsStarted  bool          `json: "isStarted"`
	Logs       [][]string    `json: "logs"`
	Name       string        `json: "name"`
	Results    []interface{} `json: "results"` // Results[1] is error code?
	// Statistics
	StepNumber float64   `json: "step_number"`
	Text       []string  `json: "text"`
	Times      []float64 `josn: "times"`
	// Urls
}

func (step StepData) IsSuccess() bool {
	if len(step.Results) == 0 {
		return false
	}
	if r, ok := step.Results[0].(float64); ok && r == 0 {
		return true
	}
	return false
}

type ChangeData struct {
	Branch   string   `json: "branch"`
	Comments string   `json: "comments"`
	Files    []string `json: "files"`
	Number   float64  `json: "number"`
	Revision string   `json: "revision"`
	RevLink  string   `json: "revlink"`
	When     float64  `json: "when"`
	Who      string   `json: "string"`
}

type SourceStampData struct {
	Branch   string       `json: "branch"`
	Changes  []ChangeData `json: "changes"`
	Revision string       `json: "revision"`
}

type BuildData struct {
	Blame       []string   `json: "blame"`
	BuilderName string     `json: "builderName"`
	Changes     []string   `json: "changes"`
	CurrentStep StepData   `json: "currentStep"`
	Eta         float64    `json: "eta"`
	Logs        [][]string `json: "logs"`
	Number      float64    `json: "number"`
	// Properties
	Reason string `json: "reason"`
	// Requests
	Slave       string          `json: "slave"`
	SourceStamp SourceStampData `json: "sourceStamp"`
	Steps       []StepData      `json: "step"`
	Text        []string        `json: "text"`
	Times       []float64       `json: "times"`
}

func createActivitiesFromBuildData(buildbot *Buildbot, build BuildData) (activities []Activity) {
	buildURL, _ := buildbot.url_.Parse(fmt.Sprintf("builders/%s/builds/%d", build.BuilderName, int(build.Number)))
	var buildIssues []int64
	var recipients []string
	var files []string
	for _, change := range build.SourceStamp.Changes {
		_, _, review := parseChangeDescription(change.Comments)
		buildIssues = append(buildIssues, getReviewIssue(review))
		recipients = append(recipients, change.Who)
		files = append(files, change.Files...)
	}

	var buildStart Activity
	buildStart.User = build.BuilderName
	buildStart.Time = time.Unix(int64(build.Times[0]), 0)
	buildStart.URL = buildURL.String()
	buildStart.Snippet = fmt.Sprintf("build start @ %s", build.SourceStamp.Revision)
	buildStart.Type = ActivityBuildStart
	buildStart.Issues = buildIssues
	buildStart.Recipients = recipients
	buildStart.Files = files

	activities = append(activities, buildStart)

	buildSuccess := true
	for _, step := range build.Steps {
		if step.IsFinished && !step.IsSuccess() {
			var buildFail Activity
			buildFail.User = build.BuilderName
			buildFail.Time = time.Unix(int64(step.Times[1]), 0)
			stepURL, _ := buildbot.url_.Parse(fmt.Sprintf("builders/%s/builds/%d/steps/%s/", build.BuilderName, int(build.Number), step.Name))
			buildFail.URL = stepURL.String()
			buildFail.Snippet = fmt.Sprintf("build failed %s: %v", step.Name, step.Text)
			buildFail.Type = ActivityBuildFailed
			buildFail.Issues = buildIssues
			buildFail.Recipients = recipients
			buildFail.Files = files
			activities = append(activities, buildFail)
			buildSuccess = false
		}
	}
	if len(build.Times) > 1 && build.Times[1] > 0 && buildSuccess {
		var buildSuccess Activity
		buildSuccess.User = build.BuilderName
		buildSuccess.Time = time.Unix(int64(build.Times[1]), 0)
		buildSuccess.URL = buildURL.String()
		buildSuccess.Snippet = "build success"
		buildSuccess.Type = ActivityBuildSuccess
		buildSuccess.Issues = buildIssues
		buildSuccess.Recipients = recipients
		buildSuccess.Files = files
		activities = append(activities, buildSuccess)
	}
	return activities
}

func printBuildData(build BuildData) {
	fmt.Printf(" builder: %v\n", build.BuilderName)
	fmt.Printf(" number: %v\n", build.Number)
	fmt.Printf(" blame: %v\n", build.Blame)
	fmt.Printf(" revision: %s\n", build.SourceStamp.Revision)
	fmt.Printf(" changes: %d [\n", len(build.Changes))
	for _, change := range build.SourceStamp.Changes {
		fmt.Printf("  rev:%s %s (%s)\n",
			change.Revision, change.RevLink,
			change.Branch)
		fmt.Printf("  who:%s\n", change.Who)
		ts := time.Unix(int64(change.When), 0)
		fmt.Printf("  when:%s\n", ts)
		summary, bug, review := parseChangeDescription(change.Comments)
		fmt.Printf("  comments:%s\n", summary)
		if bug != "" {
			fmt.Printf("  bug:%s\n", bug)
		}
		if review != "" {
			fmt.Printf("  review:%s\n", review)
		}
		fmt.Printf("  files: %v\n", change.Files)
	}
	fmt.Printf("\n ]\n")
	if build.CurrentStep.IsStarted {
		fmt.Printf(" current step: %s\n", build.CurrentStep.Name)
	}
	for _, step := range build.Steps {
		if step.IsFinished && !step.IsSuccess() {
			fmt.Printf(" failed step: %s @ %s\n",
				step.Name,
				time.Unix(int64(step.Times[1]), 0))
			for _, log := range step.Logs {
				fmt.Printf("   %s\n", log[1])
			}
		}
	}
	if len(build.Text) > 0 {
		fmt.Printf(" %v\n", build.Text)
	}
	if len(build.Times) > 0 {
		fmt.Printf(" times:\n")
		for _, t := range build.Times {
			fmt.Printf("  %s\n", time.Unix(int64(t), 0))
		}
	}
}

type SlaveData struct {
	// AccessURI string `json: "access_uri"`
	// Admin string `json: "admin"`
	// Builders map[string][]interface{} `json: "builders"`
	Connected     bool        `json: "connected"`
	Host          string      `json: "host"`
	Name          string      `json: "name"`
	RunningBuilds []BuildData `json: "runningBuilds"`
	Version       string      `json: "version"`
}

type BuildbotData struct {
	Builders map[string]BuilderData `json: "builders"`
	// ChangeSources	map[string]ChangeSourcesData `json: "change_sources"`
	Project ProjectData          `json: "project"`
	Slaves  map[string]SlaveData `json: "slaves"`
}

type Buildbot struct {
	url_ *url.URL
	Data BuildbotData
}

func NewBuildbot(path string) *Buildbot {
	buildbotUrl, _ := url.Parse(path)
	return &Buildbot{
		url_: buildbotUrl}
}

func (b *Buildbot) fetch(c *http.Client) (err error) {
	resp, err := c.Get(b.url_.String())
	if err != nil {
		return err
	}
	d := json.NewDecoder(resp.Body)
	err = d.Decode(&b.Data)
	return nil
}

func (b *Buildbot) fetchBuild(c *http.Client, builder string, buildnum int, build *BuildData) (err error) {
	burl, err := b.url_.Parse(fmt.Sprintf("json/builders/%s/builds/%d", builder, buildnum))
	if err != nil {
		return err
	}
	fmt.Println(burl)
	resp, err := c.Get(burl.String())
	if err != nil {
		return err
	}
	d := json.NewDecoder(resp.Body)
	err = d.Decode(build)
	return nil
}

func main() {
	flag.Parse()
	buildbot := NewBuildbot(chromiumBuildbot)
	fmt.Printf("%#v\n", *buildbot)

	if flagBuilder != "" {
		var build BuildData
		err := buildbot.fetchBuild(&http.Client{}, flagBuilder, flagBuildnum, &build)
		if err != nil {
			fmt.Printf("fetch err:%#v\n", err)
		}
		printBuildData(build)
		activities := createActivitiesFromBuildData(buildbot, build)
		fmt.Printf("Activities:\n%#v\n", activities)
		return
	}
	err := buildbot.fetch(&http.Client{})
	if err != nil {
		fmt.Printf("fetch err:%#v\n", err)
	}
	fmt.Printf("project: %s %s bot:%s\n",
		buildbot.Data.Project.ProjectName,
		buildbot.Data.Project.ProjectURL,
		buildbot.Data.Project.BuildbotURL)
	for k, v := range buildbot.Data.Builders {
		fmt.Printf("builder: %s\n state:%s last:%d latest:%d current:%v pending:%v\n",
			k, v.State,
			v.GetLastBuild(), v.GetLatestBuild(),
			v.CurrentBuilds, v.PendingBuilds)
	}
	for k, v := range buildbot.Data.Slaves {
		fmt.Printf("slaves: %s\n", k)
		if !v.Connected {
			fmt.Printf(" not connected\n")
		}
		for _, build := range v.RunningBuilds {
			printBuildData(build)
			activities := createActivitiesFromBuildData(buildbot, build)
			fmt.Printf("Activities:\n%#v\n", activities)
		}
		fmt.Printf("\n")
	}
}
